
package de.justphil.tcg.tcgserver.game.mechanics.property;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.commons.util.AtomicIdGenerator;
import de.justphil.tcg.tcgserver.game.mechanics.event.Event;
import de.justphil.tcg.tcgserver.game.mechanics.event.OnEventListener;
import de.justphil.tcg.tcgserver.game.mechanics.event.Scheduler;
import de.justphil.tcg.tcgserver.game.mechanics.game.BoardInfo;
import de.justphil.tcg.tcgserver.game.mechanics.game.Game;
import de.justphil.tcg.tcgserver.game.mechanics.game.GameDataSerializable;
import de.justphil.tcg.tcgserver.game.mechanics.game.GameRandom;
import de.justphil.tcg.tcgserver.game.mechanics.monster.Monster;
import de.justphil.tcg.tcgserver.game.mechanics.monster.MonsterList;
import de.justphil.tcg.tcgserver.game.mechanics.util.tools.IterationHelper;

import org.slf4j.Logger;
import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;

public abstract class Property implements OnEventListener, GameDataSerializable {
	
	private static final Logger	log									= gameLogger(Property.class);
	public static final int		PROPERTY_NONE						= 0;
	
	public static final int		PROPERTY_ATTACKER					=  1;
	public static final int		PROPERTY_DEATH_EXPLOSION			=  2;
	public static final int		PROPERTY_MULTI_ATTACK				=  3;
	public static final int		PROPERTY_PARALYZING_ATTACK			=  4;
	public static final int		PROPERTY_SLEEP_ATTACK				=  5;
	public static final int		PROPERTY_ATTACK_DAMAGE_REFLECTION	=  6;
	public static final int		PROPERTY_REGENERATION_AURA			=  7;
	public static final int		PROPERTY_RESISTANCE_AURA			=  8;
	public static final int		PROPERTY_POISON_RESISTANT			=  9;
	public static final int		PROPERTY_LAZY						= 10;
	public static final int		PROPERTY_POISONOUS					= 11;
	public static final int		PROPERTY_FURY_AURA					= 12;
	public static final int		PROPERTY_RETALIATION				= 13;
	public static final int		PROPERTY_FURY						= 14;
	public static final int		PROPERTY_PARALYZED					= 15;
	public static final int		PROPERTY_DAMAGE						= 16;
	public static final int		PROPERTY_POISON_DEFENSE_LOSS		= 17;
	public static final int		PROPERTY_RESISTANCE					= 18;
	public static final int		PROPERTY_REGENERATION				= 19;
	public static final int		PROPERTY_SLEEPING					= 20;
	
	
	private Monster				monster;
	private boolean				active;
	private Game				game;
	private PropertyList		providedProperties					= new PropertyList();
	private long				id;
	
	/**
	 * Constructor
	 * 
	 * @param game the Game
	 */
	public Property(Game game) {
	
		if (game != null) {
			this.active = false;
			this.game = game;
			this.id = AtomicIdGenerator.nextId();
		}
	}
	
	/**
	 * Returns true if a property of this Properties type can be active multiple times on the same
	 * Monster. This Method should be overwritten by a subclass if it is stackable.
	 * 
	 * @return
	 */
	public boolean isStackable() {
	
		return false;
	}
	
	/**
	 * returns true if the Property is active.
	 * 
	 * @return
	 */
	public boolean isActive() {
	
		return this.active;
	}
	
	public long getId() {
	
		return this.id;
	}
	
	public void setId(long id) {
	
		this.id = id;
	}
	
	/**
	 * 
	 * @return the type of the Property
	 */
	public abstract int getType();
	
	/**
	 * 
	 * @return a String representation of the Propertie's type.
	 */
	public String getTypeAsString() {
	
		return getClass().getSimpleName();
	}
	
	/**
	 * 
	 * @return the Monster this Property is bind to or null if not bind to any Monster
	 */
	public Monster getMonster() {
	
		return this.monster;
	}
	
	/**
	 * Returns the modification of this value This Method should be overwritten by a subclass if it
	 * is has a modification.
	 * 
	 * @return the modification
	 */
	public int getLifeModification() {
	
		return 0;
	}
	
	/**
	 * Returns the modification of this value This Method should be overwritten by a subclass if it
	 * is has a modification.
	 * 
	 * @return the modification
	 */
	public int getAttackModification() {
	
		return 0;
	}
	
	/**
	 * Returns the modification of this value This Method should be overwritten by a subclass if it
	 * is has a modification.
	 * 
	 * @return the modification
	 */
	public int getSpeedModification() {
	
		return 0;
	}
	
	/**
	 * Returns the modification of this value This Method should be overwritten by a subclass if it
	 * is has a modification.
	 * 
	 * @return the modification
	 */
	public int getDefenseModification() {
	
		return 0;
	}
	
	/**
	 * Binds the property to a certain Monster.
	 * 
	 * @param monster the Monster to bind to
	 */
	public void setMonster(Monster monster) {
	
		this.monster = monster;
	}
	
	/**
	 * Activates the property if it is no counter Property is active.
	 */
	public void activate() {
	
		if (!isActive() && !isCountered()) {
			for (Property property : getMonster().getActiveProperties()) {
				property.onPropertyActivation(this.getType());
			}
			registerEvents();
			this.active = true;
			try {
				getScheduler().scheduleFirst(
						new Event(Event.EVENT_PROPERTY_ACTIVATED, getMonster()), 0.0);
			}
			catch (TCGException ignored) {
				/* If multiple Properties are added in a row, one PROPERTY ADDED Event is
				 * sufficient. */
			}
			log.debug("Activating property ({}).", this);
			// game.onMonsterChanged(getMonster());
			try {
				for (Monster monster : handleActivation()) {
					log.debug("The activation of Property ({}) changed Monster ({}).",
							new Object[] {
									this, monster });
					game.onMonsterChanged(monster);
				}
			}
			catch (NullPointerException ignored) {
			}
		}
	}
	
	/**
	 * Deactivates the property.
	 */
	public void deactivate() {
	
		if (isActive()) {
			this.active = false;
			for (Property property : getMonster().getActiveProperties()) {
				property.onPropertyActivation(this.getType());
			}
			unregisterEvents();
			log.debug("Deactivating property ({}).", this);
			// game.onMonsterChanged(getMonster());
			try {
				for (Monster monster : handleDeactivation()) {
					log.debug("The deactivation of Property ({}) changed Monster ({}).",
							new Object[] {
									this, monster });
					game.onMonsterChanged(monster);
				}
			}
			catch (NullPointerException ignored) {
			}
		}
	}
	
	public void onPropertyActivation(int type) {
	
		int[] counterProperties = getCounterProperties();
		for (int i = 0; i < counterProperties.length; i++) {
			if (counterProperties[i] == type) {
				deactivate();
			}
		}
	}
	
	public void onPropertyDeactivation(int type) {
	
		int[] counterProperties = getCounterProperties();
		for (int i = 0; i < counterProperties.length; i++) {
			if (counterProperties[i] == type) {
				activate();
			}
		}
	}
	
	@Override
	public final void onEvent(Event event) {
	
		assert getMonster() != null : "A " + this.getClass().getSimpleName()
				+ " property is active listening to " + event.getTypeAsString()
				+ " Events even though it is not " + "bound to a monster!";
		assert game.getBoardInfo().getOwner(getMonster()) != -1 : "A "
				+ this.getClass().getSimpleName() + " property is active listening to "
				+ event.getTypeAsString() + " Events even though it is not "
				+ "bound to a monster!";
		try {
			for (Monster monster : handleEvent(event)) {
				log.debug("The Event ({}) made Property ({}) change Monster ({}).", new Object[] {
						event, this, monster });
				game.onMonsterChanged(monster);
			}
		}
		catch (NullPointerException ignored) {
		}
	}
	
	@Override
	public void writeData(Game game, DataOutput out) throws IOException {
	
		log.info("Writing Property ({}).", new Object[] {
			this });
		out.writeBoolean(active);
		out.writeInt(providedProperties.size());
		for (Property p : providedProperties) {
			game.getDataSerializableManager().writeProperty(p, game, out);
		}
	}
	
	@Override
	public void readData(Game game, DataInput in) throws IOException {
	
		log.info("Reading Property ({}).", new Object[] {
			this.id });
		this.game = game;
		active = in.readBoolean();
		if (active) {
			registerEvents();
		}
		int size = in.readInt();
		providedProperties = new PropertyList();
		for (int i = 0; i < size; i++) {
			providedProperties.add(game.getDataSerializableManager().readProperty(game, in));
		}
		monster = null;
	}
	
	@Override
	public String toString() {
	
		StringBuffer buffer = new StringBuffer();
		buffer.append("ID:");
		buffer.append(getId());
		buffer.append("|T:");
		buffer.append(getTypeAsString());
		buffer.append("|M:");
		if (getMonster() != null) {
			buffer.append(getMonster().getId());
		}
		else {
			buffer.append('-');
		}
		return new String(buffer);
	}
	
	/**
	 * Two Properties are equal if their ids are equal.
	 * 
	 */
	@Override
	public boolean equals(Object obj) {
	
		if (!Property.class.isInstance(obj)) {
			return false;
		}
		Property property = (Property) obj;
		return property.id == this.id;
	}
	
	/**
	 * return id
	 */
	@Override
	public int hashCode() {
	
		return (int) (id % Integer.MAX_VALUE);
	}
	
	protected int[] getCounterProperties() {
	
		return new int[] {};
	}
	
	/**
	 * handles the Event
	 * 
	 * @param event the Event to handle
	 * @return returns a list of all Monsters which have been changed through the Event.
	 */
	protected MonsterList handleEvent(Event event) {
	
		return null;
	}
	
	/**
	 * 
	 * @return a List of the types of all Events which are observed by this Property.
	 */
	protected List<Integer> getListeningEventTypes() {
	
		return new LinkedList<Integer>();
	}
	
	/**
	 * @return the BoardInfo to the recent game.
	 */
	protected BoardInfo getBoardInfo() {
	
		return game.getBoardInfo();
	}
	
	/**
	 * @return the Scheduler to the recent game.
	 */
	protected Scheduler getScheduler() {
	
		return game.getScheduler();
	}
	
	/**
	 * @return the GameRandom to the recent game.
	 */
	protected GameRandom getGameRandom() {
	
		return game.getGameRandom();
	}
	
	protected MonsterList handleActivation() {
	
		return null;
	}
	
	protected MonsterList handleDeactivation() {
	
		return null;
	}
	
	/**
	 * Provides a property to a Monster. Properties can be removed again by the methods
	 * withdrawProperties and withdrawAllProperties.
	 * 
	 * @param monster the monster to which the property will be provided.
	 * @param propertyType the type of the property to provide.
	 */
	protected void provideProperty(Monster monster, int propertyType) {
	
		Property property = PropertyFactory.getProperty(game, propertyType);
		if (property.getType() != PROPERTY_DAMAGE) {
			log.info("Property ({}) provides Property ({}) to Monster ({}).", new Object[] {
					this, property, monster.getId() });
		}
		this.providedProperties.add(property);
		monster.addProperty(property);
	}
	
	/**
	 * Provides a property to a Monster only if it has not already been provided by this property.If
	 * a property is removed a property of this type can be provided again. Properties can be
	 * removed again by the methods withdrawProperties and withdrawAllProperties.
	 * 
	 * @param monster the monster to which the property will be provided.
	 * @param propertyType the type of the property to provide.
	 */
	protected void providePropertyOnce(final Monster monster, final int propertyType) {
	
		final boolean[] canceled = new boolean[] {
			false };
		new IterationHelper<Property>(providedProperties) {
			
			@Override
			protected void execute(Property listElement) {
			
				if (monster.getProperties().contains(listElement)) {
					canceled[0] = true;
				}
			}
		}.safeExecuteForAll();
		if (!canceled[0]) {
			provideProperty(monster, propertyType);
		}
	}
	
	/**
	 * Withdraws all properties of a certain Monster which have been added using the provideProperty
	 * or the providePropertyOnce method.
	 * 
	 * @param monster the monster to withdraw properties of
	 */
	protected void withdrawProperties(final Monster monster) {
	
		new IterationHelper<Property>(providedProperties) {
			
			@Override
			protected void execute(Property listElement) {
			
				Property property = listElement;
				if (monster.getProperties().contains(property)) {
					if (property.getType() != PROPERTY_DAMAGE) {
						log.info("Property ({}) withdraws Property ({}) from Monster ({}).",
								new Object[] {
										Property.this, property, monster.getId() });
					}
					providedProperties.remove(property);
					monster.removeProperty(property);
				}
			}
		}.safeExecuteForAll();
	}
	
	/**
	 * Withdraws all properties all Monsters which have been added using the provideProperty or the
	 * providePropertyOnce method.
	 */
	protected MonsterList withdrawAllProperties() {
	
		final MonsterList monsters = new MonsterList();
		new IterationHelper<Property>(providedProperties) {
			
			@Override
			protected void execute(Property listElement) {
			
				Property property = listElement;
				if (property.getMonster() == null) {
					log.warn(
							"Trying to remove a Property ({}) which is not assigned to a Monster and has been provided by Property ({})!",
							new Object[] {
									property, Property.this });
				}
				else {
					if (property.getType() != PROPERTY_DAMAGE) {
						log.info("Property ({}) withdraws Property ({}) from Monster ({}).",
								new Object[] {
										Property.this, property, monster.getId() });
					}
					Monster monster = property.getMonster();
					monster.removeProperty(property);
					monsters.add(monster);
				}
			}
		}.safeExecuteForAll();
		providedProperties.clear();
		return monsters;
	}
	
	private void registerEvents() {
	
		for (int type : getListeningEventTypes()) {
			game.getEventObservable().addOnEventListener(type, this);
		}
	}
	
	private void unregisterEvents() {
	
		for (int type : getListeningEventTypes()) {
			game.getEventObservable().removeOnEventListener(type, this);
		}
	}
	
	/**
	 * Checks if the monster has an active Property which is a counter for this Property.
	 * 
	 * @return
	 */
	private boolean isCountered() {
	
		boolean countered = false;
		for (int counterType : getCounterProperties()) {
			if (monster.hasActiveProperty(counterType)) {
				countered = true;
				break;
			}
		}
		return countered;
	}
}
